iter17: A/B regression audit — Phases 0-2 - #14
Open
bfirestone wants to merge 19 commits into
Open
Conversation
Gates iter18's $400-600 90k annotation run on proof that iter16's longer SYSTEM_PROMPT did not silently regress any pre-existing detection label. Full re-run of both prompt versions on the same 5065-row input, tri-categorized report (shared/iter15-only/ iter16-only), hard agreement gate + soft prevalence warning + informational noise floor.
- Replace archived iter16 parquets as noise-floor source with a dedicated iter16b re-run (prior framing measured prompt drift from c1ec175, not same-prompt variance). - Rewrite FAIL path to reflect that annotator emits all det_* columns in a single call, so any prompt iteration requires a full iter16-side rerun (~$15); only the report filters to regressed labels. - Specify fingerprint-based row alignment against input parquet (non-det_* columns), since 5065 rows contain only 5059 unique text values. - Drop "noise floor informational only" framing; noise floor now formally bounds override eligibility via |Δagr| ≤ 2 × noise_floor. - Cap annotation concurrency at 2 jobs in flight (40 LLM requests) instead of running all sides in parallel. - Type hints use pl.DataFrame, not pd.DataFrame, to match the existing polars-only audit stack. - Expand CLI globs internally via Python glob, not the shell; assert each glob resolves to exactly 3 parquets. - Total annotation cost: ~$45 (iter15 ×1 + iter16 ×2).
- FAIL-path iteration reruns both iter16a AND iter16b (~$30), not
just iter16a. Reusing cached iter16b against a new iter16a prompt
would turn the "noise floor" back into prompt-drift measurement —
the exact mistake the Codex review caught for archived parquets,
repeated at a different layer.
- Stratified-row filter uses audit_source == "stratified"
explicitly; base cohort gets that value and injected rows use
inject_<label>, so the original audit_source=injected phrasing
would have included injected rows in agreement/prevalence metrics.
- prev_ratio 0/0 is now explicitly defined as 1.0 (no WARN), with
a table for the other zero combinations. A silent-on-both-sides
label no longer trips WARN-prevalence.
- CLI glob validation now parses filenames and asserts the model
slug set matches the canonical {gemini3flash, sonnet46, gpt54mini}
set rather than just counting 3 parquets. Catches duplicates and
misnamed files that would silently corrupt agreement estimates.
- after and noise_floor det_* column sets are hard-asserted equal,
not "assumed equal by construction." Override eligibility depends
on noise floor so a schema mismatch must fail loud.
- Added unit test cases for each new validation.
Comprehensive task-by-task TDD plan for the iter17 A/B regression audit. 16 tasks across 6 phases (P0 ops fixes → P1 audit helpers → P2 compare_prompt_versions.py module → P3 annotation runs → P4 report generation → P5 gate decision). Also aligns spec model slug to existing 'sonnet' convention (not 'sonnet46'), matching the filename pattern of the existing iter16_5k_sonnet.parquet artifact.
Restores python -m trainr.core.annotate_detections as a working invocation path. iter16 lost ~30 min diagnosing a silent exit 0 when running via python -m; the click CLI was always the canonical path but the backup diagnostic route was broken.
iter16 found 9 of 9 stack_trace 'misses' were actually regex
overmatches on Rust test directives, prose with 'at line N', and
Java frames without Exception headers. Tighten:
- Python: require 'Traceback (...)' header + 'File "' adjacency
- Java: require 'Exception in thread' header + java-frame adjacency
- Rust: (?m)^ line anchor excludes '// error-pattern:' directives
Patterns use [\s\S]{0,N} instead of (?s:.{0,N}) and (?m)^ instead
of negative lookbehind, because polars uses Rust's regex crate
which rejects both of the latter constructs.
Adds regression tests in both directions — previously-matching
anti-cases no longer match, real traces still match. Does not
rebuild iter16_5k_input.parquet.
Per-label majority-of-N fire rate on stratified rows. Needed by the iter17 A/B compare module's soft WARN-prevalence gate. Uses the same dict[model_slug, DataFrame] convention as the existing compute_agreement_across_models. Filters internally so callers don't need to remember to pre-filter to stratified rows.
Adds a test for the "label in first frame but not others" case that exercises the continue branch in compute_prevalence_per_label. Was flagged as a minor coverage gap in code review of 8f1ef7d.
Parses model slug from filename and asserts the parsed set equals
the canonical {gemini3flash, sonnet, gpt54mini} set. Rejects wrong
count, unparseable names, duplicates, and unknown slugs. Shared by
both the existing audit_semantic_labels.main() and the upcoming
compare_prompt_versions module.
The EXPECTED_MODEL_SLUGS frozenset is the single source of truth
for the canonical slug set.
Adds a test where two paths in different directories both parse to the same canonical slug (gemini3flash). Exercises the "if slug in parsed" branch in load_annotator_parquets that was flagged as a minor coverage gap in review of 99159b4.
Regenerates the iter16 audit report from the archived parquets and diffs against the committed docs/accuracy_runs report. Fails loud on any silent drift in agreement math, rounding, or output format. Insurance for the Phase 1 refactor + any future edit.
Defines LabelCategory, LabelVerdict, LabelRow, and DeltaReport dataclasses. The compare_prompt_versions() function handles the all-shared happy-path case with placeholder PASS verdicts. _compute_prev_ratio implements the spec's zero-handling rules (0/0 → 1.0, 0/>0 → inf, >0/0 → 0.0). Verdict logic, column categorization, fingerprint validation, and report emission arrive in subsequent tasks.
Dynamic det_* introspection categorizes every label as shared/iter15-only/iter16-only. Shared labels get full metrics; the other two categories get partial metrics and NO_VERDICT. Hard-asserts after and noise_floor have identical det_* column sets — the override criterion depends on noise floor, so a schema mismatch must fail loud rather than silently skip.
Implements _compute_verdict() combining the hard agreement gate (|Δagr| > 0.005) with the soft prevalence gate (prev_ratio outside [0.5, 2.0]). Verdicts can co-occur (FAIL_AND_WARN). Includes tests for each path including the zero-prevalence edge cases. Override eligibility (|Δagr| ≤ 2 × noise_floor) is NOT applied here. The verdict reflects the raw gate outcome; human review applies override logic per the Gate Decision Protocol.
Validates every annotation frame against the input parquet by comparing all non-det_* columns row-for-row. Raises ValueError naming the first diverging row index, column, and source slug on mismatch. Needed because iter16_5k_input.parquet has 5065 rows but only 5059 unique 'text' values — text-only equality is insufficient as an alignment check. Fingerprint runs BEFORE the schema assertion so row drift is caught with a precise row index rather than a vague column-set diff.
format_delta_report renders the DeltaReport with all sections from the spec: gate verdict summary, shared labels table, iter15-only and iter16-only sections, FAIL-agreement detail with override-eligibility hint (|Δagr|/noise_floor ratio), WARN-prevalence detail, and a noise floor table covering all categorized labels.
Wires compare_prompt_versions into the trainr data click group. Module main() parses argv, resolves globs via Python glob module (not the shell), loads parquets via load_annotator_parquets with slug validation, runs the comparison, and writes the markdown report. Exits 2 on any FAIL-agreement verdict so shell callers can gate on it.
Code work (Phases 0-2, 11 tasks) is done and tested. Remaining work (Phases 3-5, ~$45 LLM budget + human gate review) picked up in a future session. Handoff doc captures: - Completed task list with commit SHAs - Test coverage summary (24 compare tests + 14 audit + 6 build_audit) - Exact commands to resume at Task 3.1 (worktree setup) - Gate decision protocol reference - Pyright noise context + 10 unrelated pre-existing failures note - FAIL-path cost note ($30 per iteration, iter15 cached)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
iter17 is an A/B regression audit that gates iteration 18's full 90k annotation run on proof that iteration 16's longer
SYSTEM_PROMPTdid not silently degrade any of the 40 pre-existing detection labels.This PR delivers Phases 0-2 of the implementation plan (11 of 18 tasks): all code, tests, and the new
trainr data compare-promptsCLI. Phase 3-5 (~$45 OpenRouter annotation runs + regression report + human gate decision) is deferred to a future session — seedocs/SESSION_HANDOFF_iter17_phase2_complete.mdfor the exact resume point.Total commits on branch: 18 (4 design/plan + 13 code commits with TDD and two-stage review on each).
What's in this PR
Design (4 commits)
docs/superpowers/specs/2026-04-10-iter17-ab-regression-audit-design.mddocs/superpowers/plans/2026-04-10-iter17-ab-regression-audit.mdPhase 0 — Operational Fixes
7eafced—__main__guard onannotate_detections.py(restorespython -mbackup path)497588c— TightenedINJECTION_PATTERNS["stack_trace"]for Python/Java/Rust false positives. Polars-compatible patterns ([\s\S]{0,N}instead of(?s:.),(?m)^instead of lookbehind). Does NOT rebuilditer16_5k_input.parquet.Phase 1 — Audit Helpers (
audit_semantic_labels.py)8f1ef7d+81adffc—compute_prevalence_per_label()(majority-of-N fire rate, filters internally)99159b4+0d67183—load_annotator_parquets()+EXPECTED_MODEL_SLUGS(filename slug parsing with set-equality validation; rejects wrong count, duplicates, unknown slugs)e71e16c— Byte-for-byte reproducibility smoke test for the iter16 audit report (guards against silent drift)Phase 2 — New Module
compare_prompt_versions.py2b78826— Skeleton +DeltaReport/LabelRow/LabelVerdict/LabelCategorytypes + happy-path test37383a8— Dynamicdet_*column categorization (shared / iter15-only / iter16-only) + hard schema assertion betweenafterandnoise_floorframe sets445751c— Verdict logic: hard FAIL-agreement (|Δagr| > 0.005) + soft WARN-prevalence (prev_ratio ∉ [0.5, 2.0]) + co-occurrence (FAIL_AND_WARN) + zero-prevalence rules (0/0 → 1.0 PASS, 0/>0 → inf WARN)0846735— Row-alignment fingerprint validation (hashes all non-det_*columns row-by-row against input parquet; raisesValueErrornaming first diverging row index, column, and source slug)739757f— Pure markdownformat_delta_report()with all 8 sections from the spec's §Report Schema, including FAIL-agreement detail with override-eligibility hint (|Δagr| / noise_floorratio)62cbd6d—trainr data compare-promptsCLI wire-up with internal glob expansion via Pythonglobmodule (shell-independent). Exits2on any FAIL-agreement (report is written before exit).Test coverage
tests/test_compare_prompt_versions.pytests/test_audit_semantic_labels.pytests/test_build_audit_sample.pyAll tests green. An end-to-end smoke test of
trainr data compare-promptsagainst real iter16 parquets (running iter16 vs iter16 as both sides) produced a valid PASS report with all 42 shared labels and zero deltas — confirming the full CLI code path works against real data.Why paused before Phase 3
Phase 3 runs ~$45 of OpenRouter annotation calls (iter15 ×3 models + iter16a ×3 + iter16b ×3 noise floor). Phase 4 writes the iteration doc which requires human judgment on the regression report. Phase 5 is the gate decision per the Gate Decision Protocol. These need explicit user go-ahead and interactive error handling, so they're better run from a fresh session with the user present rather than as a long-running subagent chain. The handoff doc captures exact resume commands.
Key design decisions (from spec+plan)
c1ec175CSV-log refinement, so they measure prompt drift rather than same-prompt variance.|agr(iter16a) - agr(iter16b)|with both on the iter16 prompt at HEAD. FAIL-path iteration reruns both iter16a AND iter16b (~$30 per iteration) — caching iter16b against a new iter16a prompt would silently corrupt the noise floor.iter16_5k_input.parquethas 5065 rows but only 5059 uniquetextvalues, so text-only equality is insufficient. Hashes all non-det_*columns.{gemini3flash, sonnet, gpt54mini}is a canonical frozenset; filename regex + set-equality catches duplicates and misnames that would silently corrupt agreement estimates.|Δagr| ≤ 2 × noise_floor— noise floor bounds override eligibility but does NOT adjust the 0.005 threshold itself. Longitudinal consistency vs per-run noise calibration.Test plan (reviewer)
test_compare_prompt_versions.pypasstrainr data compare-prompts --helprenders all 5 optionstest_audit_semantic_labels.py/test_build_audit_sample.py/test_annotate_detections.pyiter16_5k_input.parquetmtime unchanged (file not rebuilt)Out of scope (iter18+)
See
docs/SESSION_HANDOFF_iter17_phase2_complete.mdfor the exact resume point when picking up Phase 3 in a future session.